/*
* Copyright (C) 2014 MediaTek Inc.
* Modification based on code covered by the mentioned copyright
* and/or permission notice(s).
*/
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.incallui;

import com.google.common.collect.Maps;
import com.google.common.base.Preconditions;

import com.mediatek.incallui.ext.IInCallScreenExt;

import android.os.Handler;
import android.os.Message;
import android.telecom.DisconnectCause;
import android.telecom.Phone;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Maintains the list of active calls and notifies interested classes of changes to the call list
 * as they are received from the telephony stack. Primary listener of changes to this class is
 * InCallPresenter.
 */
/*
 * M: implements IInCallScreenExt
 * Original code:
public class CallList implements InCallPhoneListener{
 */
public class CallList implements InCallPhoneListener, IInCallScreenExt {

    private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200;
    private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000;
    private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000;

    private static final int EVENT_DISCONNECTED_TIMEOUT = 1;

    private static CallList sInstance = new CallList();

    private final HashMap<String, Call> mCallById = new HashMap<>();
    private final HashMap<android.telecom.Call, Call> mCallByTelecommCall = new HashMap<>();
    private final HashMap<String, List<String>> mCallTextReponsesMap = Maps.newHashMap();
    /**
     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
     * load factor before resizing, 1 means we only expect a single thread to
     * access the map so make only a single shard
     */
    private final Set<Listener> mListeners = Collections.newSetFromMap(
            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
    private final HashMap<String, List<CallUpdateListener>> mCallUpdateListenerMap = Maps
            .newHashMap();

    private List<Call> mInComingCallList = new ArrayList<Call>();
    private List<Call> mHoldCallList = new ArrayList<Call>();

    private Phone mPhone;

    /**
     * Static singleton accessor method.
     */
    public static CallList getInstance() {
        return sInstance;
    }

    private Phone.Listener mPhoneListener = new Phone.Listener() {
        @Override
        public void onCallAdded(Phone phone, android.telecom.Call telecommCall) {
            Call call = new Call(telecommCall);
            if (call.getState() == Call.State.INCOMING) {
                onIncoming(call, call.getCannedSmsResponses());
            } else {
                onUpdate(call);
            }
        }
        @Override
        public void onCallRemoved(Phone phone, android.telecom.Call telecommCall) {
            if (mCallByTelecommCall.containsKey(telecommCall)) {
                Call call = mCallByTelecommCall.get(telecommCall);
                if (updateCallInMap(call)) {
                    Log.w(this, "Removing call not previously disconnected " + call.getId());
                }
                updateCallTextMap(call, null);
            }
        }

        //--------------------------------MTK---------------------------
        @Override
        public void onStorageFull() {
            for (Listener listener : mListeners) {
                listener.onStorageFull();
            }
        }

        @Override
        public void onUpdateRecordState(final int state, final int customValue) {
            Log.i(this, "onUpdateRecordState: state = " + state + " ;customValue = " + customValue);
            for (Listener listener : mListeners) {
                listener.onUpdateRecordState(state, customValue);
            }
        }
    };

    /**
     * Private constructor.  Instance should only be acquired through getInstance().
     */
    private CallList() {
    }

    @Override
    public void setPhone(Phone phone) {
        mPhone = phone;
        mPhone.addListener(mPhoneListener);
    }

    @Override
    public void clearPhone() {
        mPhone.removeListener(mPhoneListener);
        mPhone = null;
    }

    /**
     * Called when a single call disconnects.
     */
    public void onDisconnect(Call call) {
        if (updateCallInMap(call)) {
            Log.i(this, "onDisconnect: " + call);
            // update local call list
            updateIncomingCallList(call);
            updateHoldCallList(call);
            // notify those listening for changes on this specific change
            notifyCallUpdateListeners(call);
            // notify those listening for all disconnects
            notifyListenersOfDisconnect(call);
        }
    }

    /**
     * Called when a single call has changed.
     */
    public void onIncoming(Call call, List<String> textMessages) {
        if (updateCallInMap(call)) {
            Log.i(this, "onIncoming - " + call);
        }
        updateCallTextMap(call, textMessages);

        updateIncomingCallList(call);

        for (Listener listener : mListeners) {
            listener.onIncomingCall(call);
        }
    }

    /**
     * Called when a single call has changed.
     */
    public void onUpdate(Call call) {
        onUpdateCall(call);
        notifyGenericListeners();
    }

    public void notifyCallUpdateListeners(Call call) {
        final List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(call.getId());
        if (listeners != null) {
            for (CallUpdateListener listener : listeners) {
                listener.onCallChanged(call);
            }
        }
    }

    /**
     * Add a call update listener for a call id.
     *
     * @param callId The call id to get updates for.
     * @param listener The listener to add.
     */
    public void addCallUpdateListener(String callId, CallUpdateListener listener) {
        List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
        if (listeners == null) {
            listeners = new CopyOnWriteArrayList<CallUpdateListener>();
            mCallUpdateListenerMap.put(callId, listeners);
        }
        listeners.add(listener);
    }

    /**
     * Remove a call update listener for a call id.
     *
     * @param callId The call id to remove the listener for.
     * @param listener The listener to remove.
     */
    public void removeCallUpdateListener(String callId, CallUpdateListener listener) {
        List<CallUpdateListener> listeners = mCallUpdateListenerMap.get(callId);
        if (listeners != null) {
            listeners.remove(listener);
        }
    }

    public void addListener(Listener listener) {
        Preconditions.checkNotNull(listener);

        mListeners.add(listener);

        // Let the listener know about the active calls immediately.
        listener.onCallListChange(this);
    }

    public void removeListener(Listener listener) {
        if (listener != null) {
            mListeners.remove(listener);
        }
    }

    /**
     * TODO: Change so that this function is not needed. Instead of assuming there is an active
     * call, the code should rely on the status of a specific Call and allow the presenters to
     * update the Call object when the active call changes.
     */
    public Call getIncomingOrActive() {
        Call retval = getIncomingCall();
        if (retval == null) {
            retval = getActiveCall();
        }
        return retval;
    }

    public Call getOutgoingOrActive() {
        Call retval = getOutgoingCall();
        if (retval == null) {
            retval = getActiveCall();
        }
        return retval;
    }

    /**
     * A call that is waiting for {@link PhoneAccount} selection
     */
    public Call getWaitingForAccountCall() {
        return getFirstCallWithState(Call.State.PRE_DIAL_WAIT);
    }

    public Call getPendingOutgoingCall() {
        return getFirstCallWithState(Call.State.CONNECTING);
    }

    public Call getOutgoingCall() {
        Call call = getFirstCallWithState(Call.State.DIALING);
        if (call == null) {
            call = getFirstCallWithState(Call.State.REDIALING);
        }
        return call;
    }

    public Call getActiveCall() {
        return getFirstCallWithState(Call.State.ACTIVE);
    }

    public Call getBackgroundCall() {
        if (mHoldCallList.size() >= 1) {
            return mHoldCallList.get(0);
        }
        return null;
    }

    public Call getDisconnectedCall() {
        return getFirstCallWithState(Call.State.DISCONNECTED);
    }

    public Call getDisconnectingCall() {
        return getFirstCallWithState(Call.State.DISCONNECTING);
    }

    public Call getSecondBackgroundCall() {
        if (mHoldCallList.size() >= 2) {
            return mHoldCallList.get(1);
        }
        return null;
    }

    public Call getSecondaryIncomingCall() {
        if (mInComingCallList.size() >= 2) {
            return mInComingCallList.get(1);
        }
        return null;
    }

    public void switchIncomingCalls() {
        if (mInComingCallList.size() >= 2) {
            Call call = mInComingCallList.get(0);
            mInComingCallList.remove(0);
            mInComingCallList.add(1, call);
        }
        // trigger to update incoming ui.
        if (getIncomingCall() != null) {
            for (Listener listener : mListeners) {
                /// M: ALPS01791896. @{
                // Maybe need update prompt message for first incoming call.
                listener.onCallListChange(this);
                /// @}
            }
        }

        /// M: Set the sorted Incoming call ids list to Telecom for BT. @{
        TelecomAdapter.getInstance().setSortedIncomingCallList(getCallIdListByState(Call.State.INCOMING));
        /// @}
    }

    private void updateIncomingCallList(Call call) {
        // Only update the ids list for incoming calls when the list is really changed.
        boolean isIncomingCallListChanged = false;
        int state = call.getState();
        if (Call.State.isIncoming(state) && !mInComingCallList.contains(call)) {
            mInComingCallList.add(0, call);
            isIncomingCallListChanged = true;
        } else if (!Call.State.isIncoming(state)) {
            Iterator<Call> it = mInComingCallList.iterator();
            while (it.hasNext()) {
                Call oldCall = it.next();
                if (Call.areSame(oldCall, call)) {
                    Log.d(this, "updateIncomingCallList (remove)- ", call);
                    it.remove();
                    isIncomingCallListChanged = true;
                }
            }
        }

        /// M: Set the sorted Incoming call ids list to Telecom for BT. @{
        if (isIncomingCallListChanged) {
            TelecomAdapter.getInstance().setSortedIncomingCallList(
                    getCallIdListByState(Call.State.INCOMING));
        }
        /// @}
    }

    private void updateHoldCallList(Call call) {
        // Only update the ids list for hold calls when the list is really changed.
        boolean isHoldCallListChanged = false;
        int state = call.getState();
        if (state == Call.State.ONHOLD && !mHoldCallList.contains(call)
                && call.getParentId() == null) {
            // If there is no first call when current call changed from active to held, we should
            // still keep this call as primary.
            //   Eg: 1A1H --> 2H, the primary call will not change.
            // Switch call when 1A2H, there will be three hold calls and no other call, we just put
            // the third hold call at end of the list. Telecom will unhold one call of them later.
            if (getFirstCall() == null && getWaitingForAccountCall() == null
                    && mHoldCallList.size() < MAX_HOLD_CALL_NUM) {
                mHoldCallList.add(0, call);
            } else {
                mHoldCallList.add(call);
            }
            isHoldCallListChanged = true;
            Log.d(this, "updateHoldCallList (add)- ", call);
        } else if (state != Call.State.ONHOLD) {
            Iterator<Call> it = mHoldCallList.iterator();
            while (it.hasNext()) {
                Call oldCall = it.next();
                if (Call.areSame(oldCall, call)) {
                    Log.d(this, "updateHoldCallList (remove)- ", call);
                    it.remove();
                    isHoldCallListChanged = true;
                }
            }
        }

        /// M: Set the sorted background call ids list to Telecom for BT. @{
        if (isHoldCallListChanged) {
            TelecomAdapter.getInstance().setSortedBackgroudCallList(
                    getCallIdListByState(Call.State.ONHOLD));
        }
        /// @}
    }

    public Call getActiveOrBackgroundCall() {
        Call call = getActiveCall();
        if (call == null) {
            call = getBackgroundCall();
        }
        return call;
    }

    public Call getIncomingCall() {
        if (mInComingCallList.size() >= 1) {
            return mInComingCallList.get(0);
        }
        return null;
    }

    public Call getFirstCall() {
        Call result = getIncomingCall();
        if (result == null) {
            result = getPendingOutgoingCall();
        }
        if (result == null) {
            result = getOutgoingCall();
        }
        if (result == null) {
            result = getFirstCallWithState(Call.State.ACTIVE);
        }
        if (result == null) {
            result = getDisconnectingCall();
        }
        if (result == null) {
            result = getDisconnectedCall();
        }
        return result;
    }

    public boolean hasLiveCall() {
        Call call = getFirstCall();
        if (call == null) {
            return false;
        }
        return call != getDisconnectingCall() && call != getDisconnectedCall();
    }

    /**
     * Returns the first call found in the call map with the specified call modification state.
     * @param state The session modification state to search for.
     * @return The first call with the specified state.
     */
    public Call getVideoUpgradeRequestCall() {
        for(Call call : mCallById.values()) {
            if (call.getSessionModificationState() ==
                    Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
                return call;
            }
        }
        return null;
    }

    public Call getCallById(String callId) {
        return mCallById.get(callId);
    }

    public Call getCallByTelecommCall(android.telecom.Call telecommCall) {
        return mCallByTelecommCall.get(telecommCall);
    }

    public List<String> getTextResponses(String callId) {
        return mCallTextReponsesMap.get(callId);
    }

    /**
     * Returns first call found in the call map with the specified state.
     */
    public Call getFirstCallWithState(int state) {
        return getCallWithState(state, 0);
    }

    /**
     * Returns the [position]th call found in the call map with the specified state.
     * TODO: Improve this logic to sort by call time.
     */
    public Call getCallWithState(int state, int positionToFind) {
        Call retval = null;
        int position = 0;
        for (Call call : mCallById.values()) {
            if (call.getState() == state) {
                if (position >= positionToFind) {
                    retval = call;
                    break;
                } else {
                    position++;
                }
            }
        }

        return retval;
    }

    /**
     * This is called when the service disconnects, either expectedly or unexpectedly.
     * For the expected case, it's because we have no calls left.  For the unexpected case,
     * it is likely a crash of phone and we need to clean up our calls manually.  Without phone,
     * there can be no active calls, so this is relatively safe thing to do.
     */
    public void clearOnDisconnect() {
        for (Call call : mCallById.values()) {
            final int state = call.getState();
            if (state != Call.State.IDLE &&
                    state != Call.State.INVALID &&
                    state != Call.State.DISCONNECTED) {

                call.setState(Call.State.DISCONNECTED);
                call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN));
                updateCallInMap(call);
            }
        }
        notifyGenericListeners();
    }

    /**
     * Processes an update for a single call.
     *
     * @param call The call to update.
     */
    private void onUpdateCall(Call call) {
        Log.d(this, "\t" + call);
        if (updateCallInMap(call)) {
            Log.i(this, "onUpdate - " + call);
        }

        updateCallTextMap(call, call.getCannedSmsResponses());
        updateIncomingCallList(call);
        updateHoldCallList(call);
        notifyCallUpdateListeners(call);
    }

    /**
     * Sends a generic notification to all listeners that something has changed.
     * It is up to the listeners to call back to determine what changed.
     */
    private void notifyGenericListeners() {
        for (Listener listener : mListeners) {
            listener.onCallListChange(this);
        }
    }

    private void notifyListenersOfDisconnect(Call call) {
        for (Listener listener : mListeners) {
            listener.onDisconnect(call);
        }
    }

    /**
     * Updates the call entry in the local map.
     * @return false if no call previously existed and no call was added, otherwise true.
     */
    private boolean updateCallInMap(Call call) {
        Preconditions.checkNotNull(call);

        boolean updated = false;

        if (call.getState() == Call.State.DISCONNECTED) {
            // update existing (but do not add!!) disconnected calls
            if (mCallById.containsKey(call.getId())) {
                // For disconnected calls, we want to keep them alive for a few seconds so that the
                // UI has a chance to display anything it needs when a call is disconnected.

                // Set up a timer to destroy the call after X seconds.
                final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
                mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));

                mCallById.put(call.getId(), call);
                /**
                 * M:add for plug in. @{
                 */
                mCallMap.put(call.getId(), call.getTelecommCall());
                /**
                 * M:add for plug in. @}
                 */
                mCallByTelecommCall.put(call.getTelecommCall(), call);
                updated = true;
            }
        } else if (!isCallDead(call)) {
            mCallById.put(call.getId(), call);
            /**
             * M:add for plug in. @{
             */
            mCallMap.put(call.getId(), call.getTelecommCall());
            /**
             * M:add for plug in. @}
             */
            mCallByTelecommCall.put(call.getTelecommCall(), call);
            updated = true;
        } else if (mCallById.containsKey(call.getId())) {
            mCallById.remove(call.getId());
            /**
             * M:add for plug in. @{
             */
            mCallMap.remove(call.getId());
            /**
             * M:add for plug in. @}
             */
            mCallByTelecommCall.remove(call.getTelecommCall());
            updated = true;
        }

        return updated;
    }

    private int getDelayForDisconnect(Call call) {
        Preconditions.checkState(call.getState() == Call.State.DISCONNECTED);


        final int cause = call.getDisconnectCause().getCode();
        final int delay;
        switch (cause) {
            /// M: if DisconnectCause is error will update CallList quickly @{ 
            case DisconnectCause.ERROR:
            /// @}
            case DisconnectCause.LOCAL:
                delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
                break;
            case DisconnectCause.REMOTE:
                delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
                break;
            case DisconnectCause.REJECTED:
            case DisconnectCause.MISSED:
            case DisconnectCause.CANCELED:
                // no delay for missed/rejected incoming calls and canceled outgoing calls.
                delay = 0;
                break;
            default:
                delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
                break;
        }

        return delay;
    }

    private void updateCallTextMap(Call call, List<String> textResponses) {
        Preconditions.checkNotNull(call);

        if (!isCallDead(call)) {
            if (textResponses != null) {
                mCallTextReponsesMap.put(call.getId(), textResponses);
            }
        } else if (mCallById.containsKey(call.getId())) {
            mCallTextReponsesMap.remove(call.getId());
        }
    }

    private boolean isCallDead(Call call) {
        final int state = call.getState();
        return Call.State.IDLE == state || Call.State.INVALID == state;
    }

    /**
     * Sets up a call for deletion and notifies listeners of change.
     */
    private void finishDisconnectedCall(Call call) {
        call.setState(Call.State.IDLE);
        updateCallInMap(call);
        notifyGenericListeners();
    }

    /**
     * Notifies all video calls of a change in device orientation.
     *
     * @param rotation The new rotation angle (in degrees).
     */
    public void notifyCallsOfDeviceRotation(int rotation) {
        for (Call call : mCallById.values()) {
            if (call.getVideoCall() != null) {
                call.getVideoCall().setDeviceOrientation(rotation);
            }
        }
    }

    /**
     * Handles the timeout for destroying disconnected calls.
     */
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EVENT_DISCONNECTED_TIMEOUT:
                    Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
                    finishDisconnectedCall((Call) msg.obj);
                    break;
                default:
                    Log.wtf(this, "Message not expected: " + msg.what);
                    break;
            }
        }
    };

    /**
     * Listener interface for any class that wants to be notified of changes
     * to the call list.
     */
    public interface Listener {
        /**
         * Called when a new incoming call comes in.
         * This is the only method that gets called for incoming calls. Listeners
         * that want to perform an action on incoming call should respond in this method
         * because {@link #onCallListChange} does not automatically get called for
         * incoming calls.
         */
        public void onIncomingCall(Call call);

        /**
         * Called anytime there are changes to the call list.  The change can be switching call
         * states, updating information, etc. This method will NOT be called for new incoming
         * calls and for calls that switch to disconnected state. Listeners must add actions
         * to those method implementations if they want to deal with those actions.
         */
        public void onCallListChange(CallList callList);

        /**
         * Called when a call switches to the disconnected state.  This is the only method
         * that will get called upon disconnection.
         */
        public void onDisconnect(Call call);
        /// M: Add for recording. @{
        public void onStorageFull();
        public void onUpdateRecordState(final int state, final int customValue);
        /// @}
    }

    /// M: For 3G VT only @{
    public interface CallActiveListener {
        public void onActive(Call call);
    }
    /// @}

    public interface CallUpdateListener {
        // TODO: refactor and limit arg to be call state.  Caller info is not needed.
        public void onCallChanged(Call call);
    }

    // ---------------------------------MTK--------------------------------------

    private static final int MAX_HOLD_CALL_NUM = 2;

    /**
     * Added for plug in. implement call back method.@{
     */
    public void requestUpdateScreen() {
        notifyGenericListeners();
    }
    /**
     * Added for plug in.@}
     */

    /**
     * Added for plug in.Used to get telecomm call map. @{
     */
    private final HashMap<String, android.telecom.Call> mCallMap = Maps.newHashMap();
    public HashMap<String, android.telecom.Call> getCallMap() {
        return mCallMap;
    }
    /**
     * Added for plug in. @}
     */

    /**
     * Get telcom callIds for these calls with specified state.
     * @param state
     * @return
     */
    private List<String> getCallIdListByState(int state) {
        final List<String> callIdList = new ArrayList<String>();
        if (state == Call.State.INCOMING) {
            for (Call call: mInComingCallList) {
                callIdList.add(call.getTelecommCall().getCallId());
            }
        } else if (state == Call.State.ONHOLD) {
            for (Call call: mHoldCallList) {
                callIdList.add(call.getTelecommCall().getCallId());
            }
        }
        return callIdList;
    }

    /**
     * M: Get all background calls
     */
    public List<Call> getBackgroundCalls() {
        return mHoldCallList;
    }

    /**
     * M: Get the number of the active and background calls.
     * @return number of the calls with specified state.
     */
    public int getActiveAndHoldCallsCount() {
        int count = 0;
        if(getActiveCall() != null){
            count += 1;
        }
        count += mHoldCallList.size();
        return count;
    }

    /// M: For 3G VT only @{
    private final Set<CallActiveListener> mCallActiveListeners = Collections.newSetFromMap(
            new ConcurrentHashMap<CallActiveListener, Boolean>(8, 0.9f, 1));

    public void addCallActiveListener(CallActiveListener listener) {
        Preconditions.checkNotNull(listener);

        mCallActiveListeners.add(listener);
    }

    public void removeCallActiveListener(CallActiveListener listener) {
        if (listener != null) {
            mCallActiveListeners.remove(listener);
        }
    }

    public void onActive(Call call) {
        notifyListenersOfActive(call);
    }

    private void notifyListenersOfActive(Call call) {
        for (CallActiveListener listener : mCallActiveListeners) {
            listener.onActive(call);
        }
    }
    /// @}

}
